/*
 * Copyright 2014, NuzoweR@Schneefeuer <nuzower@gmail.com>.
 * The Settlers Online - Combat Simulator (tsosim).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "Simulator.h"
#include "Statistics.h"
#include <vector>
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <time.h>


class S_OUT
{
public:
	S_OUT() : stream(&std::cout) {};
	~S_OUT() {
		if(!filename.empty() && file.is_open())
		{
			file.close();
		}
	}
	std::ofstream file;
	std::string   filename;
	std::ostream* stream;
};
static S_OUT OUT;

Simulator::Parameters::Parameters()
: repeats(100),
  modify_g1(false), modify_g2(false),
  print_stats(true), print_minmax(false), print_log(false)
{};


// static initialization
Simulator::Parameters Simulator::DEFAULT;

Simulator::Simulator() : g1(NULL), g2(NULL), logging(false)
{
	PARAMS = DEFAULT;

	time_t timer;
	time(&timer);

	srand((unsigned int)timer);
}


bool Simulator::set_default_repeats(int repeats)
{
	if(repeats <= 0) return false;

	DEFAULT.repeats = repeats;
	return true;
}


bool Simulator::set_repeats(int repeats)
{
	if(repeats <= 0) return false;
	
	PARAMS.repeats = repeats;
	return true;
}

void Simulator::set_modify(bool mod_g1, bool mod_g2)
{
	PARAMS.modify_g1 = mod_g1;
	PARAMS.modify_g2 = mod_g2;
}

void Simulator::set_print_results(bool stats, bool minmax, bool log)
{
	PARAMS.print_stats  = stats;
	PARAMS.print_minmax = minmax;
	PARAMS.print_log    = log;
}


void Simulator::set_ostream(std::string const& filename)
{
	OUT.filename = filename;
}

std::ostream* Simulator::get_ostream()
{
	if(!OUT.filename.empty())
	{
		if(!OUT.file.is_open())
		{
			OUT.file.open(OUT.filename.c_str());
			if(OUT.file.is_open()) {
				OUT.stream = &OUT.file;
			}
		}
	}
	return OUT.stream;
}

bool Simulator::set_garrisons(Garrison* attacker, Garrison* defender)
{
	if(attacker == NULL || defender == NULL) return false;

	g1 = attacker;
	g2 = defender;
	return true;
}

#if 0
static std::list<int> random_numbers;

static void print_random_average() {
	long long int sum=0;
	int nr = 0;
	std::list<int>::iterator it = random_numbers.begin();
	for(; it != random_numbers.end(); ++it) {
		nr++;
		sum += *it;
	}
	if(nr == 0) return;

	double average = sum / (double)nr;
	std::cout << "ra: " << average << std::endl;
}
#endif

bool Simulator::start_combat()
{
	if(PARAMS.repeats <= 0) return false;
	if(g1 == NULL || g2 == NULL) return false;

	logging = false;

	Statistics g1_stats, g2_stats;

	g1->init_stats(&g1_stats);
	g2->init_stats(&g2_stats);

	/*
	 * statistics run
	 */
	if(PARAMS.print_stats)
	{
		for(int i=0; i < PARAMS.repeats; ++i)
		{
			// make copies
			Garrison offense = *g1;
			Garrison defense = *g2;

			CDATA.round      = 0;
			CDATA.initiative = 0;

			//LOG.clear();
			//logging = true;
	//		random_numbers.clear();
			int rounds = compute_combat(offense, defense, RM_RANDOM);
	//		print_random_average();
			//logging = false;
			//LOG.print_combat_log(5, true);


			offense.update_stats(&g1_stats, Garrison::NORMAL);
			defense.update_stats(&g2_stats, Garrison::NORMAL);

			g1_stats.rounds_data.number_iterations += 1;
			g1_stats.rounds_data.iteration_results.push_back(rounds);

			g2_stats.rounds_data.number_iterations += 1;
			g2_stats.rounds_data.iteration_results.push_back(rounds);
		}
	}

	if(PARAMS.print_stats && PARAMS.print_minmax)
	{
		/*
		 * min/max
		 */
		{
			// make copies
			Garrison offense = *g1;
			Garrison defense = *g2;

	//		random_numbers.clear();
			int rounds = compute_combat(offense, defense, RM_MINMAX);
	//		print_random_average();

			g1_stats.rounds_data.results.min = rounds;
			g2_stats.rounds_data.results.max = rounds;

			offense.update_stats(&g1_stats, Garrison::MIN);
			defense.update_stats(&g2_stats, Garrison::MAX);
		}

		/*
		 * max/min
		 */
		{
			// make copies
			Garrison offense = *g1;
			Garrison defense = *g2;

	//		random_numbers.clear();
			int rounds = compute_combat(offense, defense, RM_MAXMIN);
	//		print_random_average();

			g1_stats.rounds_data.results.max = rounds;
			g2_stats.rounds_data.results.min = rounds;

			offense.update_stats(&g1_stats, Garrison::MAX);
			defense.update_stats(&g2_stats, Garrison::MIN);
		}
	}

	if(PARAMS.print_stats)
	{
		g1_stats.ComputeAverage();
		g2_stats.ComputeAverage();

		g1_stats.Print(*get_ostream(), PARAMS.print_minmax);
		g2_stats.Print(*get_ostream(), PARAMS.print_minmax);
	}

	/*
	 * update garrison
	 */
	if(PARAMS.print_log || PARAMS.modify_g1 || PARAMS.modify_g2)
	{
		// make copies
		Garrison g1_copy = *g1;
		Garrison g2_copy = *g2;


		LOG.clear();
		logging = true;
//		random_numbers.clear();
		compute_combat(
				PARAMS.modify_g1 ? *g1 : g1_copy,
				PARAMS.modify_g2 ? *g2 : g2_copy,
				RM_RANDOM);
//		print_random_average();
		logging = false;

		if(PARAMS.print_log)
			LOG.print_combat_log(*get_ostream(), 5, true);
	}

	return true;
}


int Simulator::compute_combat(Garrison& offense, Garrison& defense, RandomnessMode randmode, int fixed_accuracy_player, int fixed_accuracy_computer)
{
	CDATA.round = 0;
	bool done = false;

	while(!done)
	{
		CDATA.round++;

		const int NUM_INIT = 4;
		Initiative::EInitiative attack_type[NUM_INIT] = { Initiative::FIRST, Initiative::SECOND, Initiative::THIRD, Initiative::LAST };

		for(int i = 0; i < NUM_INIT; ++i)
		{
			Initiative::EInitiative initiative = attack_type[i];
			CDATA.initiative = initiative;

			bool attack_happened = false;

			// offense attacks
			{
				Garrison::GroupOrder_t player_first = offense.get_attack_list_by_initiative(initiative);
				if(!player_first.empty())
				{
					bool log_attack = false;
									
					// iterate over all groups and let them all attack
					Garrison::GroupOrder_t::const_iterator iter = player_first.begin();
					for(; iter != player_first.end(); ++iter)
					{
						if((*iter)->number < 1)
							continue;

						if(!log_attack) {
							if(logging) LOG.start_attack(CDATA.round, CDATA.initiative, &offense, &defense);
							log_attack = true;
						}

						Unit const& attacker_unit = *(*iter)->unit;

						RandomnessMode rmode = randmode == RM_RANDOM ? RM_RANDOM : RM_FIXED_ACCURACY;
						int fixed_accuracy = 100;
						if( randmode == RM_FIXED_ACCURACY )
							fixed_accuracy = fixed_accuracy_player;
						else if( randmode == RM_MINMIN || randmode == RM_MINMAX )
							fixed_accuracy = 100;
						else if( randmode == RM_MAXMIN || randmode == RM_MAXMAX )
							fixed_accuracy = 0;


						// attack the computer garrison, groups will be modified depending on the attack results
						this->compute_attack_on_garrison(**iter, defense, rmode, fixed_accuracy);
						attack_happened = true;
					}
				}
			}

			// defense strikes back
			{
				Garrison::GroupOrder_t computer_first = defense.get_attack_list_by_initiative(initiative);
				if(!computer_first.empty())
				{
					bool log_attack = false;

					// iterate over all groups and let them all attack
					Garrison::GroupOrder_t::iterator iter = computer_first.begin();
					for(; iter != computer_first.end(); ++iter)
					{
						if((*iter)->number < 1)
							continue;

						if(!log_attack) {
							if(logging) LOG.start_attack(CDATA.round, CDATA.initiative, &defense, &offense);
							log_attack = true;
						}

						Unit const& attacker_unit = *(*iter)->unit;

						RandomnessMode rmode = randmode == RM_RANDOM ? RM_RANDOM : RM_FIXED_ACCURACY;
						int fixed_accuracy = 100;
						if( randmode == RM_FIXED_ACCURACY )
							fixed_accuracy = fixed_accuracy_computer;
						else if( randmode == RM_MINMIN || randmode == RM_MAXMIN )
							fixed_accuracy = 100;
						else if( randmode == RM_MINMAX || randmode == RM_MAXMAX )
							fixed_accuracy = 0;


						// attack the computer garrison, groups will be modified depending on the attack results
						this->compute_attack_on_garrison(**iter, offense, rmode, fixed_accuracy);
						attack_happened = true;
					}
				}
			}

			// -----

			if(attack_happened)
			{
				offense.update_garrison_after_initiative();
				defense.update_garrison_after_initiative();
			}

			// -----

			if(offense.has_units_with_hitpoints() == 0) {
				done = true;
				break;
			}

			if(defense.has_units_with_hitpoints() == 0) {
				done = true;
				break;
			}

		}

	}

	// restore hitpoints of surviving units
	offense.update_garrison_after_combat();
	defense.update_garrison_after_combat();

	return CDATA.round;
}


/**
 * type     : what kind of units attacks
 * number   : how many units of $type attack
 * garrison : enemy units -> number of units might change after an attack !!!
 */
void Simulator::compute_attack_on_garrison(Garrison::Group& attacker_unit, Garrison& garrison, RandomnessMode randmode, int fixed_accuracy)
{
	// determine which unit should be attacked
	Garrison::GroupOrder_t groups = attacker_unit.unit->skills.hasSkill(Skill::ATTACK_WEAKEST) ? garrison.get_defend_list_weakest() : garrison.get_defend_list_normal();

	int defense_bonus = garrison.getTowerBonus();

	// iterate over groups in garrisons and attack with given unit type
	Garrison::GroupOrder_t::iterator cit = groups.begin();
	for(; cit != groups.end(); ++cit)
	{
		if((**cit).number_after_attack < 1) continue;

		if(logging) LOG.add_group_attack(&attacker_unit, (*cit), attacker_unit.number_left_to_attack, (*cit)->number);

		(*cit)->number_after_attack = this->compute_attack_on_unitgroup(attacker_unit, **cit, defense_bonus, randmode, fixed_accuracy);

		if(logging) LOG.finish_group_attack((*cit)->number_after_attack);

		if((*cit)->number_after_attack > 0)
		{
			// stop here
			break;
		}
	}
}



/*
 *
 */
int Simulator::compute_attack_on_unitgroup(Garrison::Group& attacker_group, Garrison::Group& defender_group, int defense_bonus, RandomnessMode randmode, int fixed_accuracy) // const
{
	// if we start with 0 defenders, then we can just abort the whole "fight"
	if(defender_group.number_after_attack < 1) return 0;

	/* ---- */

	struct {
		int att_id;
		int def_id;
		int damage;
		int hp_left;
	} logdata;

	// start processing defender units back to front
	int current_def = defender_group.number_after_attack - 1;
	int current_att = attacker_group.number_left_to_attack;

	//for(int i=0; i < attacker_group.number_left_to_attack; ++i)
	while(current_att > 0)
	{
		if(current_def < 0) {
			// attacker wins, no defenders left
			return 0;
		}

		logdata.att_id = attacker_group.number - current_att + 1;

		// get random number for accuracy
		int random_number;
		if(randmode == RM_FIXED_ACCURACY)
			random_number = fixed_accuracy % 101; // use %100 just for the case
		else // if(randmode == RM_RANDOM) // make sure to set a fixed value in calling function if RM_MINMAX (or similar) was selected
			random_number = rand() % 101;

//		random_numbers.push_back(random_number);

		// determine damage value
		int damage = attacker_group.damage_left;
		if(damage == 0)
			damage = (random_number <= attacker_group.unit->accuracy) ? attacker_group.unit->max_damage : attacker_group.unit->min_damage;

		int bonus_damage_pc = attacker_group.unit->damage_bonus.getBonus(defender_group.unit->enemy_unit_type);
		if(bonus_damage_pc > 0)
		{
			damage = (int)(damage * (100+bonus_damage_pc)/100.0);
		}

		if(defender_group.unit->skills.hasSkill(Skill::TOWER_BONUS) && (defense_bonus > 0))
		{
			int def_bonus = defense_bonus - (attacker_group.unit->skills.hasSkill(Skill::ARMOR_PENETRATION) ? 10 : 0);
			if(def_bonus < 0) def_bonus = 0;
			damage = (int)(damage * (100-def_bonus)/100.0);
		}

		// TODO : take tower defense into consideration

		if( attacker_group.unit->skills.hasSkill(Skill::SPLASH_DAMAGE) )
		{
			// deal splash damage
			int damage_left = damage;
			while( (damage_left > 0) && (current_def >= 0))
			{
				logdata.def_id = defender_group.number_left_to_attack - current_def;

				if(defender_group.hitpoints[current_def] <= damage_left)
				{
					logdata.damage = defender_group.hitpoints[current_def];

					damage_left -= defender_group.hitpoints[current_def]; // here assuming that the stored hitpoints for this unit are actually positive !!!!!!!
					defender_group.hitpoints[current_def] = 0;

					if(logging) LOG.add_unit_attack(attacker_group.unit, defender_group.unit, logdata.att_id, logdata.def_id, logdata.damage, defender_group.hitpoints[current_def]);

					current_def--;

				}
				else
				{
					logdata.damage = damage_left;

					defender_group.hitpoints[current_def] -= damage_left;
					damage_left = 0;

					if(logging) LOG.add_unit_attack(attacker_group.unit, defender_group.unit, logdata.att_id, logdata.def_id, logdata.damage, defender_group.hitpoints[current_def]);
				}
			}

			if(damage_left > 0) {
				attacker_group.damage_left = damage_left;
			} else {
				attacker_group.damage_left = 0;
				current_att--;
			}
		}
		else
		{
			logdata.def_id = defender_group.number_left_to_attack - current_def;
//			logdata.def_id = defender_group.number_after_attack - current_def;
			logdata.damage = damage;

			// deal regular damage
			defender_group.hitpoints[current_def] -= damage;

			if(logging) LOG.add_unit_attack(attacker_group.unit, defender_group.unit, logdata.att_id, logdata.def_id, logdata.damage, defender_group.hitpoints[current_def]);

			if(defender_group.hitpoints[current_def] <= 0) 
			{
				// defender is dead
				defender_group.hitpoints[current_def] = 0; // make sure value is 0 or positive (just for the case)
				current_def--;
			}
			current_att--;
		}
		attacker_group.number_left_to_attack = current_att;
	}

	// return number of surviving defenders
	return current_def >= 0 ? (current_def+1) : 0; 
}
